package com.zh.system.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zh.common.exception.BusinessException;
import com.zh.common.utils.RedisUtil;
import com.zh.system.config.MinioConfig;
import com.zh.web.domain.system.dto.DeleteFileDto;
import com.zh.system.domain.dto.MergeChunkDto;
import com.zh.system.domain.dto.UploadChunkDto;
import com.zh.system.domain.dto.UploadFileDto;
import com.zh.system.domain.entity.SysFile;
import com.zh.system.domain.tmp.UpLoadChunkTmp;
import com.zh.system.mapper.SysFileMapper;
import com.zh.system.service.FileService;
import com.zh.system.utils.MinioUtil;
import io.minio.ComposeSource;
import io.minio.ListObjectsArgs;
import io.minio.Result;
import io.minio.errors.*;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements FileService {
    private static final String MD5_PREFIX = "ls:md5:";
    private static final String TMP_PREFIX = "tmp";
    private final MinioConfig minioConfig;
    private final RedisUtil redisUtil;
    private final MinioUtil minioUtil;
    private final SysFileMapper sysFileMapper;
    private Long operator;
    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

    @Override
    public String saveFile(UploadFileDto uploadFileDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        String md5 = DigestUtil.md5Hex(uploadFileDto.getFile().getInputStream());
        String existsUrl = checkFileExist(md5);
        if (StrUtil.isNotEmpty(existsUrl)) {
            return existsUrl;
        }
        minioUtil.createBucket(minioConfig.getBucketName());
        String type = getSuffix(uploadFileDto.getFileName());
        String fileName = type + "/" + uploadFileDto.getFileName();
        UpLoadChunkTmp upLoadChunkTmp = UpLoadChunkTmp.builder()
                .bucketName(minioConfig.getBucketName())
                .objectName(fileName)
                .stream(uploadFileDto.getFile().getInputStream())
                .chunkSize(uploadFileDto.getFile().getSize())
                .build();
        minioUtil.upLoadChunk(upLoadChunkTmp);
        String url = minioUtil.getUrl(minioConfig.getBucketName(), fileName);
        // 存储数据库
        long size = uploadFileDto.getFile().getSize();
        SysFile sysFile = SysFile.builder()
                .url(url)
                .fileName(uploadFileDto.getFileName())
                .fileSize((double) size)
                .md5(md5)
                .type(type)
                .build();
        sysFileMapper.insert(sysFile);
        log.info("文件上传成功，fileName:{},url：{}", uploadFileDto.getFileName(), url);
        return url;
    }

    @Override
    @Transactional
    public String deleteFileByUrl(DeleteFileDto deleteFileDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        String url = deleteFileDto.getUrl();
        if (StrUtil.isEmpty(url)) {
            throw new BusinessException("url不能为空");
        }
        SysFile sysFile = BeanUtil.copyProperties(deleteFileDto, SysFile.class);
        LambdaUpdateWrapper<SysFile> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(SysFile::getUrl, sysFile.getUrl());
        boolean success = sysFileMapper.delete(updateWrapper) > 0;
        if (success) {
            //删除文件
            String objectName = url.split(minioConfig.getBucketName())[1].substring(1);
            minioUtil.deleteObject(minioConfig.getBucketName(), objectName);
            return "删除成功";
        }
        return "操作错误";
    }

    @Override
    @Transactional
    public String saveChunk(UploadChunkDto uploadChunkDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        //1.redis是否存在这个文件Md5或分片md5，存在直接返回分片md5代表已上传，不存在在进行以下步骤
        if (checkChunk(uploadChunkDto.getFileMd5()) || checkChunk(uploadChunkDto.getChunkMd5())) {
            return uploadChunkDto.getChunkMd5();
        }
        //2.上传分片
        String suffix = getSuffix(uploadChunkDto.getFileName());
        minioUtil.createBucket(minioConfig.getBucketName());
        UpLoadChunkTmp upLoadChunkTmp = UpLoadChunkTmp.builder()
                .bucketName(minioConfig.getBucketName())
                .objectName(TMP_PREFIX + "/" + uploadChunkDto.getFileMd5() + "." + suffix + "/" + uploadChunkDto.getChunkMd5() + "." + suffix)
                .chunkName(uploadChunkDto.getChunkMd5())
                .chunkSize(uploadChunkDto.getChunk().getSize())
                .stream(uploadChunkDto.getChunk().getInputStream())
                .build();
        minioUtil.upLoadChunk(upLoadChunkTmp);
        //3.上传分片后保存到redis
        String chunkKey = MD5_PREFIX + uploadChunkDto.getChunkMd5();
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("fileName", uploadChunkDto.getFileName());
        map.put("fileMd5", uploadChunkDto.getFileMd5());
        map.put("chunkMd5", uploadChunkDto.getChunkMd5());
        redisUtil.setHash(chunkKey, map, 7, TimeUnit.DAYS);
        log.info("上传分片成功，chunkMd5:{},fileName: {}", uploadChunkDto.getChunkMd5(), uploadChunkDto.getFileName());
        return uploadChunkDto.getChunkMd5();
    }

    @Override
    @Transactional
    public String mergeChunk(MergeChunkDto mergeChunkDto) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        //1.检查redis或mysql是否存在该md5的url，存在直接返回url
        if (ObjectUtil.isNotEmpty(checkFileExist(mergeChunkDto.getFileMd5()))) {
            return checkFileExist(mergeChunkDto.getFileMd5());
        }
        Iterable<Result<Item>> listedObjects = minioUtil
                .getClient()
                .listObjects(ListObjectsArgs.builder()
                        .bucket(minioConfig.getBucketName())
                        .prefix(TMP_PREFIX + "/" + mergeChunkDto.getFileMd5() + "." + getSuffix(mergeChunkDto.getFileName()) + "/")
                        .build());
        //遍历所有分片 计算总大小
        double fileSize = 0.0;
        List<ComposeSource> sourceObjectList = new ArrayList<>();
        for (Result<Item> object : listedObjects) {
            Item item = object.get();
            fileSize += item.size();
            sourceObjectList.add(ComposeSource.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(item.objectName()).build());
        }
        if (fileSize == 0.0 || sourceObjectList.isEmpty()) {
            throw new BusinessException("文件上传异常！");
        }
        //对分片进行排序
        sourceObjectList.sort(
                (o1, o2) -> {
                    String o1Name = o1.object();
                    String o2Name = o2.object();
                    Integer o1Index = Integer.parseInt(o1Name.split("-")[1].split("\\.")[0]);
                    Integer o2Index = Integer.parseInt(o2Name.split("-")[1].split("\\.")[0]);
                    return o1Index - o2Index;
                }
        );
        //文件夹名->文件类型
        String type = getSuffix(mergeChunkDto.getFileName());
        // 调用 composeObject 方法合并分片
        String fileName = type + "/" + mergeChunkDto.getFileName();
        minioUtil.composeChunk(minioConfig.getBucketName(), fileName, sourceObjectList);
        //获取合并后的对象地址
        String url = minioUtil.getUrl(minioConfig.getBucketName(), fileName);
        // 存储数据库
        SysFile sysFile = SysFile.builder()
                .url(url)
                .fileName(mergeChunkDto.getFileName())
                .fileSize(fileSize)
                .md5(mergeChunkDto.getFileMd5())
                .type(type)
                .build();
        operator = mergeChunkDto.getUpdateOperator();
        sysFile.setCreateOperator(operator);
        sysFile.setUpdateOperator(operator);
        sysFileMapper.insert(sysFile);
        String fileKey = MD5_PREFIX + mergeChunkDto.getFileMd5();
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("url", url);
        map.put("fileSize", fileSize);
        redisUtil.setHash(fileKey, map, 31, TimeUnit.DAYS);
        //删除分片文件
        EXECUTOR.execute(() -> {
            List<DeleteObject> deleteObjects = new ArrayList<>();
            for (Result<Item> object : listedObjects) {
                Item item;
                try {
                    item = object.get();
                } catch (ErrorResponseException | XmlParserException | ServerException | NoSuchAlgorithmException |
                         IOException | InvalidResponseException | InvalidKeyException | InternalException |
                         InsufficientDataException e) {
                    throw new RuntimeException(e);
                }
                deleteObjects.add(new DeleteObject(item.objectName()));
            }
            minioUtil.deleteChunk(minioConfig.getBucketName(), deleteObjects, fileName);
            redisUtil.deleteByPrefix(MD5_PREFIX + mergeChunkDto.getFileMd5() + "-");
        });
        log.info("合并分片成功，fileName:{},url:{}", mergeChunkDto.getFileName(), url);
        return url;
    }

    @Override
    public Boolean checkChunk(String chunkMd5) {
        String chunkKey = MD5_PREFIX + chunkMd5;
        if (redisUtil.exists(chunkKey)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    @Override
    public String checkFileExist(String fileMd5) {
        //1. 校验文件是否已上传 秒传 前端接收到原文件的md5代表已存储，无需再传，但是需要判断名称不同，不存在在进行以下步骤
        //1.1.redis是否存在这个文件的md5，存在直接返回 url
        String fileKey = MD5_PREFIX + fileMd5;
        if (redisUtil.exists(fileKey)) {
            Map<String, Object> hashEntries = redisUtil.getHashEntries(fileKey);
            String url = (String) hashEntries.get("url");
            redisUtil.setHash(fileKey, hashEntries, 7, TimeUnit.DAYS);
            return url;
        } else {
            //1.2.数据库是否存在这个文件的md5，存在直接返回url
            LambdaQueryWrapper<SysFile> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysFile::getMd5, fileMd5);
            SysFile sysFile = sysFileMapper.selectOne(queryWrapper);
            if (ObjectUtil.isNotNull(sysFile) && ObjectUtil.isNotEmpty(sysFile)) {
                Map<String, Object> map = MapUtil.createMap(HashMap.class);
                map.put("url", sysFile.getUrl());
                map.put("fileSize", sysFile.getFileSize());
                sysFile.setUpdateTime(LocalDateTime.now());
                sysFile.setUpdateOperator(operator);
                sysFileMapper.updateById(sysFile);
                redisUtil.setHash(fileKey, map, 7, TimeUnit.DAYS);
                return sysFile.getUrl();
            }
        }
        return null;
    }

    private String getSuffix(String fileName) {
        try {
            return fileName.substring(fileName.lastIndexOf(".") + 1);
        } catch (StringIndexOutOfBoundsException e) {
            return "";
        }
    }
}
